package core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

/**
 * Class describing a dinucleotide.
 * It is a word of 2 letters in {A, C, G, T}
 * 
 */
public class Dinucleotide {
    /** Possible letters for the dinucleotide word **/
    public static final char[] letters = {'A', 'C', 'G', 'T'};
    
    public static int nbOperations = 0;
    
    /** This dinucleotide word **/
    private String word;
    
    /**
     * Creates a new dinucleotide.
     * @param word the two letters of the dinucleotide
     * @throws Exception if the letters or the length is not correct
     */
    public Dinucleotide(String word) /*throws Exception*/ {
        // Checks the length
        if(word.length() == 2) {
            // Checks if the letters are correct
            for(int i = 0; i < 2; i++) {
                boolean found = false;
                for(int j = 0; j < letters.length; j++) {
                    if(word.charAt(i) == letters[j]) {
                        found = true; break;
                    }
                }
                if(!found) {
                    //throw new Exception("Erreur dans les lettres du dinucléotide : "+word);
                }
            }
            this.word = word;
        }
    }
    
    /**
     * Returns the mirror of the dinucleotide.
     * @return 
     */
    public String getMirror() {
        return ("" + this.word.charAt(1) + this.word.charAt(0));
    }
    
    public String getConjugate() {
        return this.getMirror();
    }
    
    public String getComplementary() {
        String res = "";
        
        for(int i=0; i<this.word.length() ; i++) {
            if(this.word.charAt(i) == 'A') { res += 'T'; }
            if(this.word.charAt(i) == 'C') { res += 'G'; }
            if(this.word.charAt(i) == 'G') { res += 'C'; }
            if(this.word.charAt(i) == 'T') { res += 'A'; }
        }
        
        return res;        
    }
    
    /**
     * Returns the letters of the dinucleotide.
     * @return
     */
    public String getWord() {
        return this.word;
    }
    
    @Override
    public boolean equals(Object o) {
        if(this == o) { return true; }
        if(!(o instanceof Dinucleotide)) { return false; }
        Dinucleotide d = (Dinucleotide)o;
        return (this.word.equals(d.getWord()));
    }
    
    /**
     * Determine if the list of dinucleotides is a maximal circular code.
     * @param list the dinucleotide list
     * @return true or false, if the dinucleotide is a maximal circular code or not.
     */
    public static boolean isMaximalCircularCode(ArrayList<Dinucleotide> list) {
        boolean res = false;
        
        // Checks the size of the list first
        // Definition : A maximum dinucleotide circular code is a dinucleotide circular code having 6 elements.
        // Definition : There are 24 maximum dinucleotide circular codes.
        if(list.size() == 6) {
            // All dinucleotide maximum circular codes are matching : X = {ij, ih, ik, jh, jk, hk}
            // With i, j, k, h belonging to {A, C, G, T}
            char i = ' ', j = ' ', k = ' ', h = ' ';

            // 1. First step
            // trying to find 'i' by counting first letters and finding the one which occurs thrice
            // also try to find 'j' by counting first letters and finding the one which occurs twice
            int nb[] = {0, 0, 0, 0};
            for(int a = 0; a < Dinucleotide.letters.length; a++) {
                for(int b = 0; b < list.size(); b++) {
                    if(list.get(b).getWord().charAt(0) == Dinucleotide.letters[a]) {
                        nb[a]++;
                    }
                }
            }   
            
            for(int a = 0; a < nb.length; a++) {
                if(nb[a] == 3) {
                    i = Dinucleotide.letters[a];
                } else if(nb[a] == 2) {
                    j = Dinucleotide.letters[a];
                }
            }
            
            // 2. Second step
            // trying to find 'k' by counting second letters and finding the one which occurs thrice
            // also try to find 'h' by counting second letters and finding the one which occurs twice
            nb[0] = 0; nb[1] = 0; nb[2] = 0; nb[3] = 0;
            for(int a = 0; a < Dinucleotide.letters.length; a++) {
                for(int b = 0; b < list.size(); b++) {
                    if(list.get(b).getWord().charAt(1) == Dinucleotide.letters[a]) {
                        nb[a]++;
                    }
                }
            }   
            
            for(int a = 0; a < nb.length; a++) {
                if(nb[a] == 3) {
                    k = Dinucleotide.letters[a];
                } else if(nb[a] == 2) {
                    h = Dinucleotide.letters[a];
                }
            }
            
            try {
                // 3. Third step 
                // Verify that i is associated to j, h and k
                // Verify that k is associated to i, j and h
                // Verify that j is associated to h
                if(list.contains(new Dinucleotide(""+i+j)) 
                && list.contains(new Dinucleotide(""+i+h)) 
                && list.contains(new Dinucleotide(""+i+k))
                && list.contains(new Dinucleotide(""+j+k))
                && list.contains(new Dinucleotide(""+h+k))
                && list.contains(new Dinucleotide(""+j+h))) {
                    res = true;
                }
            } catch (Exception ex) {}
        }
        
        return res;
    }
    
    /**
     * Determine the distance between two maximal codes.
     * @param list1 a maximal dinucleotide list
     * @param list2 a maximal dinucleotide list
     * @return the distance between the two codes
     * @pre-requisite two lists with no duplicate elements
     */
    public static int getWordsDistance(ArrayList<Dinucleotide> list1, ArrayList<Dinucleotide> list2) {
        int res = 0;
        
        if(list1.size()>list2.size()) {
            res = list1.size()-list2.size();
        }
        else {
            res = list2.size()-list1.size();
        }
        
        boolean different = true;
        for(int i=0;i<list1.size();i++) {
            different = true;
            
            for(int j=0;j<list2.size();j++) {
                if(list1.get(i).equals(list2.get(j))) {
                    different = false; break;
                }                 
            }
            
            if(different) {
                //If no equivalent word has been found
                ++res;
            }
        }
        
        return res;
    }
    
    /**
     * Determine the distance between two dinucleotides.
     * @param d1 a dinucleotide
     * @param d2 a dinucleotide
     * @return the distance between the two dinucleotide
     */
    public static int getDistance(Dinucleotide d1, Dinucleotide d2) {
        int res = 0;
        for(int i=0; i<d1.word.length(); i++) {
            if(d1.word.charAt(i)!=d2.word.charAt(i)) {
                res++;
            }
        }
        return res;
    }
    
    /**
     * Checks whether the dinucleotide is really composed of two letters
     * @return 
     */
    public boolean isDinucleotide() {
        return this.word.length()==2;
    }
    
    /**
     * Find the different conjuguates Dinucleotid in the given list and create new lists for each new one encountered
     * @param dinucleotides
     * @return 
     */
    public static ArrayList<ArrayList<Dinucleotide>> findConjugates(ArrayList<Dinucleotide> dinucleotides) {
        Dinucleotide current;
        ArrayList<ArrayList<Dinucleotide>> liste = new ArrayList<ArrayList<Dinucleotide>>();
        Dinucleotide positions[] = new Dinucleotide[dinucleotides.size()];
        for(int i=0; i<dinucleotides.size(); i++) {
            positions[i] = null;
        }
        liste.add(dinucleotides);
        for(int i=0; i<liste.get(0).size(); i++) {
            current = new Dinucleotide(dinucleotides.get(i).getConjugate());
            for(int j=i+1; j<liste.get(0).size(); j++) {
                if(current.getWord().equals(dinucleotides.get(j).getWord())) {
                    //Si le conjugué de l'élément courant existe dans la liste
                    positions[i] = new Dinucleotide(current.getWord());
                }
            }
        }
        for(int i=0; i<dinucleotides.size(); i++) {
            if(positions[i]!=null) {
                //Si des éléments sont à mettre à jour
                int taille = liste.size();  //taille de la liste avant modification
                for(int j=0; j<taille; j++) {
                    liste.add(new ArrayList<Dinucleotide>(liste.get(j)));
                    liste.get(j).remove(new Dinucleotide(positions[i].getConjugate()));
                    liste.get(liste.size()-1).remove(positions[i]);
                }
            }
        }
        
        // Ordering lexicographicaly
        for(int i=0; i<liste.size(); i++) {
            Collections.sort(liste.get(i), new DinucleotideComparator());
        }
        Collections.sort(liste,new Comparator<ArrayList<Dinucleotide>>() {
            @Override
            public int compare(ArrayList<Dinucleotide> d, ArrayList<Dinucleotide> d1) {
                // Concat words and compare
                String s1 = "", s2 = "";
                for(int i = 0; i < d.size(); i++) {
                    s1 += d.get(i);
                    s2 += d1.get(i);
                }
                
                return s1.compareTo(s2);
            }
        });
        
        System.out.println("Création des codes\n" + liste);
        return liste;
    }
    
    /**
     * Remove a letter from every Trinucleotid to create a list of Dinucleotids
     * @param trinucleotides
     * @param position
     * @return 
     */
    public static ArrayList<Dinucleotide> removeLetter(ArrayList<Trinucleotide> trinucleotides, int position) {
        nbOperations = 0;
        ArrayList<Dinucleotide> liste = new ArrayList<Dinucleotide>();
        for(int i=0; i<trinucleotides.size(); i++) {
            //On traverse l'ensemble des trinucléotides donnés en entrée
            switch(position) {
                case 0:
                    //On enlève la première lettre
                    liste.add(new Dinucleotide(""
                            +trinucleotides.get(i).getWord().charAt(1)
                            +trinucleotides.get(i).getWord().charAt(2)));
                    break;
                case 1:
                    //On enlève la deuxième lettre
                    liste.add(new Dinucleotide(""
                            +trinucleotides.get(i).getWord().charAt(0)
                            +trinucleotides.get(i).getWord().charAt(2)));
                    break;
                case 2:
                    //On enlève la dernière lettre
                    liste.add(new Dinucleotide(""
                            +trinucleotides.get(i).getWord().charAt(0)
                            +trinucleotides.get(i).getWord().charAt(1)));
                    break;
                default:
                    break;
            }
        }
        nbOperations++;
        System.out.println("Suppression de la lettre "+(position+1)+" de chaque mot\n" + liste);
        return liste;
    }
    
    /**
     * Removes the elements present many times in the same list
     * @param list the dinucleotide list
     */
    public static void removeDoubles(ArrayList<Dinucleotide> list) {
        Dinucleotide current;
        int total = 0;
        for(int i=0; i<list.size(); i++) {
            current = list.get(i);
            for(int j=i+1; j<list.size(); j++) {
                //We compare the current dinucleotide with every one placed
                //after in the list (precedents have already been compared)
                if(current.equals(list.get(j))) {
                    list.remove(j);
                    total++;
                    nbOperations++;
                    j--;    //We decrement j to update the position because of the last removal
                }
            }
        }
        System.out.println("Suppression des doublons ("+total+")\n" + list);
    }
    
    /**
     * Removes the dinucleotides with the same letters
     * @param list the dinucleotide list
     */
    public static void removeDuplicata(ArrayList<Dinucleotide> list) {
        int total = 0;
        for(int i=0; i<list.size(); i++) {
            if(list.get(i).getWord().charAt(0)==list.get(i).getWord().charAt(1)) {
                //Si les deux premières lettres sont égales
                list.remove(i);
                total++;
                nbOperations++;
                --i;    //We decrement i to update the position because of the last removal
            }
        }
        System.out.println("Suppression des dinucleotides périodiques ("+total+")\n" + list);
    }
    
    /**
     * Remove codes from the list which are not maximum circular one
     * @param codes 
     * @return removed codes
     */
    public static ArrayList<ArrayList<Dinucleotide>> removeNonMaximalCircularCodes(ArrayList<ArrayList<Dinucleotide>> codes) {
        ArrayList<ArrayList<Dinucleotide>> removed = new ArrayList<ArrayList<Dinucleotide>>();
        for(int i=0; i<codes.size(); i++) {
            if(!Dinucleotide.isMaximalCircularCode(codes.get(i))) {
                removed.add(codes.get(i));
                codes.remove(i);
                --i;
            }
        }
        System.out.println("Suppression des codes non maximaux circulaires\n" + removed);
        return removed;
    }
    
    /**
     * Generate a list containing every Dinucleotide maximal circular codes
     * @return 
     */
    public static ArrayList<ArrayList<Dinucleotide>> generateMaximalCircularCodes() {
        ArrayList<ArrayList<Dinucleotide>> list = new ArrayList<ArrayList<Dinucleotide>>();
        ArrayList<Dinucleotide> sub_list = new ArrayList<Dinucleotide>();
        ArrayList<Character> char_i = new ArrayList<Character>();
        ArrayList<Character> char_j = new ArrayList<Character>();
        ArrayList<Character> char_k = new ArrayList<Character>();
        ArrayList<Character> char_h = new ArrayList<Character>();
        char_i.add(Dinucleotide.letters[0]);
        char_i.add(Dinucleotide.letters[1]);
        char_i.add(Dinucleotide.letters[2]);
        char_i.add(Dinucleotide.letters[3]);
        for(int i=0; i<char_i.size(); i++) {
            char_j = new ArrayList<Character>(char_i);
            char_j.remove(i);
            for(int j=0; j<char_j.size(); j++) {
                char_k = new ArrayList<Character>(char_j);
                char_k.remove(j);
                for(int k=0; k<char_k.size(); k++) {
                    char_h = new ArrayList<Character>(char_k);
                    char_h.remove(k);
                    sub_list = new ArrayList<Dinucleotide>();
                    sub_list.add(new Dinucleotide(""+char_i.get(i)+char_j.get(j)));
                    sub_list.add(new Dinucleotide(""+char_i.get(i)+char_h.get(0)));
                    sub_list.add(new Dinucleotide(""+char_i.get(i)+char_k.get(k)));
                    sub_list.add(new Dinucleotide(""+char_j.get(j)+char_h.get(0)));
                    sub_list.add(new Dinucleotide(""+char_j.get(j)+char_k.get(k)));
                    sub_list.add(new Dinucleotide(""+char_h.get(0)+char_k.get(k)));
                    list.add(sub_list);
                }
            }
        }
        
        // Sorting the list lexicographicaly
        for(int i=0; i<list.size(); i++) {
            Collections.sort(list.get(i), new DinucleotideComparator());
        }
        Collections.sort(list,new Comparator<ArrayList<Dinucleotide>>() {
            @Override
            public int compare(ArrayList<Dinucleotide> d, ArrayList<Dinucleotide> d1) {
                // Concat words and compare
                String s1 = "", s2 = "";
                for(int i = 0; i < d.size(); i++) {
                    s1 += d.get(i);
                    s2 += d1.get(i);
                }
                
                return s1.compareTo(s2);
            }
        });
        
        return list;
    }
    
    private static ArrayList<ArrayList<Dinucleotide>> getMaximalCircularCodesWithout(ArrayList<ArrayList<Dinucleotide>> toRemove) {
        ArrayList<ArrayList<Dinucleotide>> list = Dinucleotide.generateMaximalCircularCodes();
        for(int i = 0; i < toRemove.size(); i++) {
            list.remove(toRemove.get(i));
        }
        return list;
    }
    
    public static int getMaximalCircularCodeNumber(ArrayList<Dinucleotide> code) {
        ArrayList<ArrayList<Dinucleotide>> list = Dinucleotide.generateMaximalCircularCodes();
        
        return list.indexOf(code);
    }
    
    /**
     * Analyze a dinucleotides list with the dinucleotides list of the maximal circular codes
     * @param liste
     * @param maximal_codes 
     */
    public static void analyzeLists(ArrayList<ArrayList<Dinucleotide>> liste, ArrayList<ArrayList<Dinucleotide>> maximal_codes) {        
        System.out.println();
        int compteur = 0;
        for(int i=0; i<maximal_codes.size(); i++) {
            if(liste.contains(maximal_codes.get(i))) {
                compteur++;
                System.out.println("X" + (i+1) + " a été trouvé : " + maximal_codes.get(i));
            }
        }
        System.out.println("\n"+compteur+" X trouvé(s)");
    }
    
    @Override
    public String toString() {
        return " "+this.word+" ";
    }
    
    public static void main(String args[]) throws Exception {
        ArrayList<ArrayList<Dinucleotide>> maximals = Dinucleotide.generateMaximalCircularCodes();
        
        ArrayList<ArrayList<Trinucleotide>> toTest = new ArrayList<ArrayList<Trinucleotide>>();
        toTest.add(Trinucleotide.getY());
        toTest.add(Trinucleotide.getY1());
        toTest.add(Trinucleotide.getY2());
        
        ArrayList<Integer> positionsToTest = new ArrayList<Integer>();
        positionsToTest.add(0);
        positionsToTest.add(1);
        positionsToTest.add(2);
        
        for(int i = 0; i < toTest.size(); i++) {
            for(int j = 0; j < positionsToTest.size(); j++) {
                ArrayList<ArrayList<Dinucleotide>> results = searchDinucleotides(toTest.get(i), positionsToTest.get(j));
                analyzeLists(results, maximals);
                System.out.println(nbOperations+" opérations\n\n");
            }
        }
    }
    
    /**
     * Search the dinucleotides present in the give trinucleotides list
     * @param list the dinucleotide list
     */
    public static ArrayList<ArrayList<Dinucleotide>> searchDinucleotides(ArrayList<Trinucleotide> trinucleotides, int letterPos) {
        System.out.println("Recherche de dinucléotides à partir du code maximal de trinucléotides\n"+trinucleotides);
        
        ArrayList<Dinucleotide> dinucleotides = removeLetter(trinucleotides,letterPos);
        
        // Remove same letters dinucleotides
        removeDuplicata(dinucleotides);
        
        // Remove doubles in the list
        removeDoubles(dinucleotides);
        
        // Find codes which can be obtained with the list of dinucleotides
        ArrayList<ArrayList<Dinucleotide>> result = findConjugates(dinucleotides);
        
        // Remove non-maximal codes from the list of codes found
        ArrayList<ArrayList<Dinucleotide>> nonCodes = removeNonMaximalCircularCodes(result);
        // Compute levenshtein distance on non-Codes
        for(int i = 0; i < nonCodes.size(); i++) {
            LevenshteinDistance.computeMinLevenshteinDistance(nonCodes.get(i), Dinucleotide.getMaximalCircularCodesWithout(result));
        }
        
        System.out.println("\nCodes maximaux circulaires restants\n" + result);
        return result;
    }
}

class DinucleotideComparator implements Comparator<Dinucleotide> {
    @Override
    public int compare(Dinucleotide d, Dinucleotide d1) {
        return d.getWord().compareTo(d1.getWord());
    }
}